"""
    BUFRLibWrapper

Low-level Julia FFI interface to NCEP BUFRLIB (libbufr_julia.so).

This module provides Julia bindings to the Fortran BUFRLIB library for reading
PrepBUFR files. It wraps key BUFRLIB functions through ISO_C_BINDING interfaces
and handles Fortran string conventions and unit number management.

# Key Functions

- `openbf!`: Open BUFR file for reading
- `closbf!`: Close BUFR file
- `ufbint!`: Read/write data values from/to a BUFR subset
- `ufbrep!`: Read/write replicated sequences
- `ufbseq!`: Read/write data using sequence descriptors
- `ireadmg`: Read next BUFR message
- `ireadsb`: Read next BUFR subset
- `getbmiss`: Get BUFR missing value indicator

# Usage

```julia
using GSICoreAnalysis.DataIO.BUFRLibWrapper

# Initialize BUFR library
bufr = BUFRLibHandle()

# Open PrepBUFR file
unit = 10
open_bufr_file!(bufr, unit, "prepbufr.file", "IN")

# Read data
while read_message!(bufr, unit, subset_name, idate) == 0
    while read_subset!(bufr, unit) == 0
        # Extract data with ufbint
        hdr_data = read_bufr_data!(bufr, unit, "SID XOB YOB DHR", 8, 1)
        # Process data...
    end
end

# Clean up
close_bufr_file!(bufr, unit)
```

# Thread Safety

**IMPORTANT**: BUFRLIB is NOT thread-safe. Always set `JULIA_NUM_THREADS=1`
when using this module, or ensure proper locking if multithreading is required.

# References

- NCEP BUFRLIB: https://github.com/NOAA-EMC/NCEPLIBS-bufr
- GSI PrepBUFR processing: comGSIv3.7/src/read_prepbufr.f90
"""
module BUFRLibWrapper

using Printf
using Libdl

export BUFRLibHandle
export open_bufr_file!, close_bufr_file!
export read_message!, read_subset!
export read_bufr_data!, get_bufr_missing_value
export BUFRDataArray

# Path to BUFRLIB shared library
const BUFRLIB_PATH = let
    # Try local build first
    local_lib = normpath(joinpath(@__DIR__, "..", "..", "deps", "native_bufr",
                                   "bufrlib_build", "libbufr_julia.so"))
    if isfile(local_lib)
        local_lib
    else
        # Fallback to environment or standard location
        get(ENV, "BUFRLIB_PATH", "/usr/local/lib/libbufr_julia.so")
    end
end

# Fortran BUFR missing value (typically 10^10)
const BUFR_MISSING_THRESHOLD = 1.0e9

"""
    BUFRLibHandle

Handle for managing BUFRLIB state and library access.

# Fields
- `lib_path::String`: Path to BUFRLIB shared library
- `lib_handle::Ptr{Cvoid}`: DL handle to loaded library
- `lib_loaded::Bool`: Whether library has been successfully loaded
- `missing_value::Float64`: BUFR missing value indicator
- `open_units::Set{Int}`: Set of currently open BUFR unit numbers
"""
mutable struct BUFRLibHandle
    lib_path::String
    lib_handle::Ptr{Cvoid}
    lib_loaded::Bool
    missing_value::Float64
    open_units::Set{Int}

    function BUFRLibHandle(lib_path::String = BUFRLIB_PATH)
        if !isfile(lib_path)
            @warn "BUFRLIB not found at: $lib_path"
            return new(lib_path, C_NULL, false, 1.0e10, Set{Int}())
        end

        # Try to load library and get missing value
        try
            lib_handle = Libdl.dlopen(lib_path, Libdl.RTLD_NOW | Libdl.RTLD_GLOBAL)
            getbmiss_sym = Libdl.dlsym(lib_handle, :getbmiss_)
            missing_val = ccall(getbmiss_sym, Float64, ())
            @info "BUFRLIB loaded successfully" path=lib_path missing_value=missing_val
            return new(lib_path, lib_handle, true, missing_val, Set{Int}())
        catch e
            @warn "Failed to load BUFRLIB" exception=e
            return new(lib_path, C_NULL, false, 1.0e10, Set{Int}())
        end
    end
end

"""
    BUFRDataArray

Container for BUFR data arrays read from subsets.

# Fields
- `data::Matrix{Float64}`: Data array (mnemonic_count × level_count)
- `nret::Int`: Number of levels actually read
- `mnemonics::Vector{String}`: List of mnemonics requested
"""
struct BUFRDataArray
    data::Matrix{Float64}
    nret::Int
    mnemonics::Vector{String}
end

"""
    get_bufr_missing_value(lib_path::String = BUFRLIB_PATH) -> Float64

Get the BUFR missing value indicator from the library.

This calls the Fortran function `getbmiss()`.
"""
function get_bufr_missing_value(lib_path::String = BUFRLIB_PATH)
    if !isfile(lib_path)
        return 1.0e10
    end

    # Call getbmiss_ (Fortran with UNDERSCORE convention)
    # Use dlopen/dlsym to avoid ccall limitation with local variables
    try
        lib = Libdl.dlopen(lib_path)
        sym = Libdl.dlsym(lib, :getbmiss_)
        missing_val = ccall(sym, Float64, ())
        Libdl.dlclose(lib)
        return missing_val
    catch e
        @warn "Failed to call getbmiss" exception=e
        return 1.0e10
    end
end

"""
    open_bufr_file!(handle::BUFRLibHandle, unit::Int, filename::String, mode::String;
                   table_unit::Union{Int,Nothing} = nothing)

Open a BUFR file for reading or writing.

# Arguments
- `handle`: BUFRLIB handle
- `unit`: Fortran logical unit number for the BUFR file
- `filename`: Path to BUFR file
- `mode`: "IN" for input, "OUT" for output
- `table_unit`: Optional unit number for BUFR table file (if different from `unit`)

# Notes
- If `table_unit` is provided, the table file must already be opened on that unit
- Mode "IN" opens for reading (most common for PrepBUFR)
- This calls the Fortran subroutine `openbf`
"""
function open_bufr_file!(handle::BUFRLibHandle, unit::Int, filename::String, mode::String;
                        table_unit::Union{Int,Nothing} = nothing)
    if !handle.lib_loaded
        error("BUFRLIB not loaded: $(handle.lib_path)")
    end

    if unit in handle.open_units
        @warn "BUFR unit $unit already open; closing first"
        close_bufr_file!(handle, unit)
    end

    # Open the file with Fortran I/O
    # Fortran: open(unit=unit, file=filename, form='unformatted', status='old', action='read')
    file_unit = open(filename, "r")

    # Prepare Fortran strings (null-terminated)
    mode_fortran = mode * "\0"

    # Determine table unit (use same unit if not specified)
    tbl_unit = something(table_unit, unit)

    # Call openbf (Fortran signature: subroutine openbf(lunit, io, lundx))
    # Note: Fortran expects integers passed by reference
    try
        ccall((:openbf_, handle.lib_path), Cvoid,
              (Ref{Int32}, Ptr{UInt8}, Ref{Int32}),
              Int32(unit), pointer(mode_fortran), Int32(tbl_unit))

        push!(handle.open_units, unit)
        @debug "Opened BUFR file" unit=unit filename=filename mode=mode
    catch e
        close(file_unit)
        error("Failed to call openbf: $e")
    end

    return file_unit
end

"""
    close_bufr_file!(handle::BUFRLibHandle, unit::Int)

Close a BUFR file.

# Arguments
- `handle`: BUFRLIB handle
- `unit`: Fortran logical unit number to close

# Notes
- Calls the Fortran subroutine `closbf`
- Safe to call even if unit is not open
"""
function close_bufr_file!(handle::BUFRLibHandle, unit::Int)
    if !handle.lib_loaded
        return
    end

    if !(unit in handle.open_units)
        @debug "BUFR unit $unit not open; skipping closbf"
        return
    end

    try
        ccall((:closbf_, handle.lib_path), Cvoid, (Ref{Int32},), Int32(unit))
        delete!(handle.open_units, unit)
        @debug "Closed BUFR file" unit=unit
    catch e
        @warn "Failed to call closbf" unit=unit exception=e
    end
end

"""
    read_message!(handle::BUFRLibHandle, unit::Int, subset::Vector{UInt8}, idate::Ref{Int32}) -> Int

Read the next BUFR message from the file.

# Arguments
- `handle`: BUFRLIB handle
- `unit`: Fortran logical unit number
- `subset`: 8-byte character array to receive subset name
- `idate`: Reference to integer to receive message date (YYYYMMDDHH)

# Returns
- 0 if message read successfully
- -1 if end of file reached
- Other non-zero values indicate errors

# Notes
- Calls the Fortran function `ireadmg`
- `subset` must be pre-allocated with at least 8 bytes
"""
function read_message!(handle::BUFRLibHandle, unit::Int, subset::Vector{UInt8},
                      idate::Ref{Int32})
    if !handle.lib_loaded
        error("BUFRLIB not loaded")
    end

    # Call ireadmg (Fortran signature: integer function ireadmg(lunit, subset, idate))
    try
        iret = ccall((:ireadmg_, handle.lib_path), Int32,
                    (Ref{Int32}, Ptr{UInt8}, Ref{Int32}),
                    Int32(unit), pointer(subset), idate)
        return Int(iret)
    catch e
        error("Failed to call ireadmg: $e")
    end
end

"""
    read_subset!(handle::BUFRLibHandle, unit::Int) -> Int

Read the next BUFR subset from the current message.

# Arguments
- `handle`: BUFRLIB handle
- `unit`: Fortran logical unit number

# Returns
- 0 if subset read successfully
- -1 if no more subsets in current message
- Other non-zero values indicate errors

# Notes
- Calls the Fortran function `ireadsb`
- Must call `read_message!` first to position at a message
"""
function read_subset!(handle::BUFRLibHandle, unit::Int)
    if !handle.lib_loaded
        error("BUFRLIB not loaded")
    end

    # Call ireadsb (Fortran signature: integer function ireadsb(lunit))
    try
        iret = ccall((:ireadsb_, handle.lib_path), Int32, (Ref{Int32},), Int32(unit))
        return Int(iret)
    catch e
        error("Failed to call ireadsb: $e")
    end
end

"""
    read_bufr_data!(handle::BUFRLibHandle, unit::Int, mnemonic_str::String,
                   max_mnemonics::Int, max_levels::Int) -> BUFRDataArray

Read data values from the current BUFR subset using mnemonics.

# Arguments
- `handle`: BUFRLIB handle
- `unit`: Fortran logical unit number
- `mnemonic_str`: Space-separated mnemonic string (e.g., "SID XOB YOB DHR")
- `max_mnemonics`: Maximum number of mnemonics (dimension 1)
- `max_levels`: Maximum number of vertical levels (dimension 2)

# Returns
- `BUFRDataArray` containing data matrix and metadata

# Notes
- Calls the Fortran subroutine `ufbint`
- Data is returned as a matrix of size (max_mnemonics × max_levels)
- Missing values are set to BUFR missing value indicator
- Number of levels actually read is returned in `nret` field

# Example
```julia
# Read header data (single level)
hdr = read_bufr_data!(handle, unit, "SID XOB YOB DHR TYP ELV", 8, 1)
lon = hdr.data[2, 1]  # XOB is 2nd mnemonic
lat = hdr.data[3, 1]  # YOB is 3rd mnemonic

# Read multi-level observation data
obs = read_bufr_data!(handle, unit, "POB UOB VOB", 10, 250)
for k in 1:obs.nret
    pressure = obs.data[1, k]
    u_wind = obs.data[2, k]
    v_wind = obs.data[3, k]
end
```
"""
function read_bufr_data!(handle::BUFRLibHandle, unit::Int, mnemonic_str::String,
                        max_mnemonics::Int, max_levels::Int)
    if !handle.lib_loaded
        error("BUFRLIB not loaded")
    end

    # Parse mnemonics
    mnemonics = split(mnemonic_str)

    # Allocate data array (Fortran column-major order)
    data = fill(handle.missing_value, max_mnemonics, max_levels)

    # Prepare Fortran string (80 characters, space-padded)
    mnem_fortran = rpad(mnemonic_str, 80)[1:80] * "\0"

    # Return value for number of levels
    iret = Ref{Int32}(0)

    # Call ufbint (Fortran signature:
    # subroutine ufbint(lunit, arr, mxmn, mxlv, iret, mnemonic_str))
    try
        ccall((:ufbint_, handle.lib_path), Cvoid,
              (Ref{Int32}, Ptr{Float64}, Ref{Int32}, Ref{Int32}, Ref{Int32}, Ptr{UInt8}),
              Int32(unit), pointer(data), Int32(max_mnemonics), Int32(max_levels),
              iret, pointer(mnem_fortran))

        return BUFRDataArray(data, Int(iret[]), collect(String, mnemonics))
    catch e
        error("Failed to call ufbint: $e")
    end
end

"""
    is_missing(handle::BUFRLibHandle, value::Float64) -> Bool

Check if a value is the BUFR missing value indicator.

# Arguments
- `handle`: BUFRLIB handle
- `value`: Value to check

# Returns
- `true` if value is missing (or exceeds missing threshold)
- `false` otherwise
"""
function is_missing(handle::BUFRLibHandle, value::Float64)
    return !isfinite(value) || value >= handle.missing_value - BUFR_MISSING_THRESHOLD
end

"""
    set_date_format!(handle::BUFRLibHandle, format::Int)

Set the date format for BUFR message dates.

# Arguments
- `handle`: BUFRLIB handle
- `format`: Date format (8 or 10 for YYYYMMDD or YYYYMMDDHH)

# Notes
- Calls the Fortran subroutine `datelen`
- Default is typically 10 (YYYYMMDDHH)
"""
function set_date_format!(handle::BUFRLibHandle, format::Int)
    if !handle.lib_loaded
        error("BUFRLIB not loaded")
    end

    try
        ccall((:datelen_, handle.lib_path), Cvoid, (Ref{Int32},), Int32(format))
        @debug "Set BUFR date format" format=format
    catch e
        @warn "Failed to call datelen" exception=e
    end
end

"""
    bufr_test_available() -> Bool

Test if BUFRLIB is available and functional.

# Returns
- `true` if BUFRLIB can be loaded and basic functions work
- `false` otherwise
"""
function bufr_test_available()
    try
        handle = BUFRLibHandle()
        return handle.lib_loaded
    catch
        return false
    end
end

end # module BUFRLibWrapper
